package org.durandj.ystream.client.ui;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractButton;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;

import org.durandj.ystream.client.YStreamClient;
import org.durandj.ystream.client.YStreamClientListener;
import org.durandj.ystream.client.YStreamPlayer;
import org.durandj.ystream.client.message.Message;
import org.durandj.ystream.client.message.MessageHandler;
import org.durandj.ystream.client.message.MessageID;
import org.durandj.ystream.client.message.Message.Priority;
import org.durandj.ystream.client.util.ByteBufferUtils;
import org.durandj.ystream.client.util.Settings;

import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;


/**
 * The main client window.
 * 
 * @author durandj
 * @version 1.0
 *
 */
public class YStreamFrame extends javax.swing.JFrame implements YStreamClientListener
{
	// TODO: Adjust the file filters!
	// TODO: Add full screen capabilities!
	// TODO: Hotkeys for menu items!
	// TODO: Double clicking in the playlist should play that item.
	// TODO: Add shuffle?
	// TODO: We need to add volume and muting buttons!
	// TODO: I think this class should be rewritten to get rid of the Jigloo code...
	
	{
		//Set Look & Feel
		try
		{
			javax.swing.UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}

	private static final long serialVersionUID = -4876574586697529782L;
	
	/**
	 * This is used to calculate the new size of the <code>scrollPlaylist</code> component of the player.
	 */
	private static final int PLAYLIST_HEIGHT_GROWTH_FACTOR = 188;
	
	/**
	 * This is used to calculate the new location of the <code>btnAdd</code> component of the player.
	 */
	private static final int ADD_BUTTON_PLACEMENT_FACTOR = 185;
	
	/**
	 * This is used to calculate the new location of the <code>btnRemove</code> component of the player.
	 */
	private static final int REMOVE_BUTTON_PLACEMENT_FACTOR = 154;
	
	/**
	 * This is used to calculate the new location of the <code>btnUp</code> component of the player.
	 */
	private static final int UP_BUTTON_PLACEMENT_FACTOR = 123;
	
	/**
	 * This is used to calculate the new location of the <code>btnDown</code> component of the player.
	 */
	private static final int DOWN_BUTTON_PLACEMENT_FACTOR = 92;
	
	/**
	 * This is used to calculate the new size of the <code>textTitle</code> component of the player.
	 */
	private static final int FILE_TITLE_GROWTH_FACTOR = 278;
	
	/**
	 * This is used to calculate the new size of the <code>canvasScreen</code> component of the player.
	 */
	private static final int SCREEN_GROWTH_X_FACTOR = 278;
	
	/**
	 * This is used to calculate the new size of the <code>canvasScreen</code> component of the player.
	 */
	private static final int SCREEN_GROWTH_Y_FACTOR = 196;
	
	/**
	 * This is used to calculate the new location of the <code>panelControls</code> component of the player.
	 */
	private static final int CONTROLS_PLACEMENT_FACTOR = 153;
	
	/**
	 * This is used to calculate the new size of the <code>panelControls</code> component of the player.
	 */
	private static final int CONTROLS_GROWTH_FACTOR = 278;
	
	// TODO: Need to add some JavaDoc to these fields!
	private JMenu menuOptions;
	private JPanel panelPlaylist;
	private JButton btnAdd;
	private JButton btnDown;
	private JButton btnUp;
	private JButton btnRemove;
	private JSlider trackbar;
	private JMenuItem menuExit;
	private JSeparator menuOptionsSeparator1;
	private JMenuItem menuConnect;
	private JMenuItem menuPlayFile;
	private JSeparator menuOptionsSeparator2;
	private JMenuItem menuSettings;
	private JSeparator menuOptionsSeparator3;
	private JMenuItem menuAbout;
	private JButton btnBack;
	private JButton btnStop;
	private JButton btnNext;
	private JButton btnPlay;
	private JPanel panelControls;
	private Canvas canvasScreen;
	private JTextField textTitle;
	private JList playlist;
	private JScrollPane scrollPlaylist;
	private JPanel panelPlayer;
	private JMenuBar menubar;
	
	/**
	 * A list of the message handlers that monitor this class.
	 */
	private List<MessageHandler> observers;
	
	/**
	 * Used to add and remove files from the playlist.
	 */
	private DefaultListModel playlistModel;
	
	/**
	 * A reference to the music player.
	 */
	private YStreamPlayer player;
	
	/**
	 * Listens to events from the player.
	 */
	private MediaPlayerEventListener playerListener;
	
	/**
	 * When a song is playing this class monitors the current location and updates the trackbar
	 * accordingly.
	 */
	private Thread positionMonitor;

	/**
	 * Constructs the frame and gives the frame access to the player.
	 * 
	 * @param player - the music player
	 */
	public YStreamFrame()
	{
		super();
		
		initGUI();
		
		this.observers = new ArrayList<MessageHandler>();
		this.player = null;
		YStreamClient.getClient().addListener(this);
		
		this.positionMonitor = new Thread(new Runnable() {
			@SuppressWarnings("static-access")
			@Override
			public void run()
			{
				// Before we do anything we first need to wait for the player to be set and initialized.
				while(player == null)
				{
					// Since we aren't impatient, let's wait a second before we check again.
					try
					{
						positionMonitor.sleep(100);
					}
					catch(InterruptedException e)
					{
						return;
					}
				}
				
				// We are going to keep running and checking things while this thread has not been interrupted.
				// The only time this thread should be interrupted is when the window is closing.
				do
				{
					// Is the player even playing?
					if(!player.isPlaying() || player.isPaused())
					{
						// We only need to check intermittently so let's wait a bit.
						try
						{
							positionMonitor.sleep(100);
						}
						catch(InterruptedException e)
						{
							return;
						}
					}
					
					// It's playing so lets get the current position and set the trackbar.
					trackbar.setValue((int)(1000 * player.getPosition()));
					
					// Just to keep from checking too often lets wait a moment.
					try
					{
						positionMonitor.sleep(75);
					}
					catch(InterruptedException e)
					{
						return;
					}
				}
				while(!positionMonitor.isInterrupted());
			}
		}, "MediaPlayerMonitor");
		positionMonitor.start();
	}
	
	/**
	 * Initializes GUI components.
	 */
	private void initGUI()
	{
		try
		{
			BorderLayout thisLayout = new BorderLayout();
			getContentPane().setLayout(thisLayout);
			this.setTitle("YStream");
			{
				menubar = new JMenuBar();
				setJMenuBar(menubar);
				{
					menuOptions = new JMenu();
					menubar.add(menuOptions);
					menuOptions.setText("Options");
					menuOptions.setPopupMenuVisible(true);
					{
						menuPlayFile = new JMenuItem();
						menuOptions.add(menuPlayFile);
						menuPlayFile.setText("Play file(s)");
						menuPlayFile.setToolTipText("Play a file on the local machine.");
						
						menuPlayFile.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent arg0)
							{
								JFileChooser fc = new JFileChooser();
								fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
								fc.setMultiSelectionEnabled(true);
								
								// Check if the user hit cancel.
								if(fc.showOpenDialog(YStreamFrame.this) == JFileChooser.CANCEL_OPTION)
									return;
								
								player.queueMessage(new Message(Priority.MEDIUM, MessageID.PLAYER_STOP, this));
								clearPlaylist();
								
								// TODO: Do something to make sure it is playable?
								
								for(File f : fc.getSelectedFiles())
									addToPlaylist(f);
								
								// TODO: Enforce that this actually happens!
								player.queueMessage(new Message(Priority.LOW, MessageID.PLAYER_PLAY, YStreamFrame.this));
							}
						});
					}
					{
						menuConnect = new JMenuItem();
						menuOptions.add(menuConnect);
						menuConnect.setText("Connect to server");
						menuConnect.setToolTipText("Connect to a YStream server to get a playlist.");
						
						menuConnect.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent arg0)
							{
								// Since this menu option servers two purposes we need to do some checking.
								// If the client is currently streaming from a server we need to assume
								// that they want to disconnect. If they are not streaming from a server
								// then they want to start so we get that process going.
								if(YStreamClient.getClient().isStreaming())
								{
									YStreamClient.getClient().queueMessage(new Message(Priority.MEDIUM,
									                                                   MessageID.DISCONNECT,
									                                                   YStreamFrame.this));
								}
								else
								{
									ServerConnectDialog dlg = new ServerConnectDialog(YStreamFrame.this);
									
									// Since the user may have connected to a server before let's pull that
									// data from the settings file and fill it in for the user.
									if(Settings.getSettings().getSetting("last_server_address") != null &&
									   Settings.getSettings().getSetting("last_server_port") != null)
									{
										dlg.setServerAddress(Settings.getSettings().getSetting("last_server_address"));
										dlg.setServerPort(Integer.parseInt(Settings.getSettings().getSetting("last_server_port")));
									}
									
									dlg.setVisible(true);
									
									if(dlg.getResult() != ServerConnectDialog.RESULT_CONNECT)
										return;
									
									ByteBuffer contents = ByteBuffer.allocate(2 * dlg.getServerAddress().length() + 6);
									ByteBufferUtils.addString(contents, dlg.getServerAddress());
									contents.putInt(dlg.getServerPort());
									YStreamClient.getClient().queueMessage(new Message(Priority.MEDIUM,
									                                                   MessageID.CONNECT,
									                                                   YStreamFrame.this,
									                                                   contents));
									
									// Now we store the connection information for later use.
									Settings.getSettings().addSetting("last_server_address", dlg.getServerAddress());
									Settings.getSettings().addSetting("last_server_port", Integer.toString(dlg.getServerPort()));
								}
							}
						});
					}
					{
						menuOptionsSeparator1 = new JSeparator();
						menuOptions.add(menuOptionsSeparator1);
					}
					{
						menuAbout = new JMenuItem();
						menuOptions.add(menuAbout);
						menuAbout.setText("About");
						menuAbout.setToolTipText("About the YStream client.");
						
						menuAbout.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent arg0)
							{
								AboutDialog dlg = new AboutDialog(YStreamFrame.this);
								dlg.setVisible(true);
							}
						});
					}
					{
						menuOptionsSeparator2 = new JSeparator();
						menuOptions.add(menuOptionsSeparator2);
					}
					{
						menuSettings = new JMenuItem();
						menuOptions.add(menuSettings);
						menuSettings.setText("Settings");
						menuSettings.setToolTipText("Changes your settings.");
						
						menuSettings.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent arg0)
							{
								SettingsDialog dlg = new SettingsDialog(YStreamFrame.this);
								dlg.setVisible(true);
								
								// Check if they canceled what they did.
								if(dlg.getResult() == SettingsDialog.RESULT_CANCEL)
									return;
								
								// They want us to apply the changes so lets get to work.
								
								// Since changing the VLC directory requires the player to stop lets not change it unless needed.
								if(!dlg.getVLCDirectory().equals(Settings.getSettings().getSetting("vlc_path")))
								{
									// We have to change the VLC settings so we first need to stop the player.
									player.queueMessage(new Message(Priority.HIGH, MessageID.PLAYER_STOP, YStreamFrame.this));
									
									// TODO: There should be a stall to make sure that the player is actually stopped before we make
									// TODO: any changes.
									
									// It is now safe to make the changes.
									Settings.getSettings().setSetting("vlc_path", dlg.getVLCDirectory());
									
									// We now start the player up again.
									YStreamClient.getClient().queueMessage(new Message(Priority.HIGH, MessageID.CLIENT_RELOAD_PLAYER, YStreamFrame.this));
								}
							}
						});
					}
					{
						menuOptionsSeparator3 = new JSeparator();
						menuOptions.add(menuOptionsSeparator3);
					}
					{
						menuExit = new JMenuItem();
						menuOptions.add(menuExit);
						menuExit.setText("Exit");
						menuExit.setToolTipText("Exit the YStream client.");
						
						menuExit.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent arg0)
							{
								notifyObs(new Message(Priority.HIGH, MessageID.KILL, this));
							}
						});
					}
				}
			}
			{
				panelPlaylist = new JPanel();
				getContentPane().add(panelPlaylist, BorderLayout.EAST);
				panelPlaylist.setPreferredSize(new java.awt.Dimension(250, 439));
				panelPlaylist.setLayout(null);
				{
					scrollPlaylist = new JScrollPane();
					panelPlaylist.add(scrollPlaylist);
					scrollPlaylist.setBounds(0, 0, 247, 312);
					{
						playlist = new JList();
						scrollPlaylist.setViewportView(playlist);
						playlistModel = new DefaultListModel();
						playlist.setModel(playlistModel);
						playlist.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
						playlist.setToolTipText("Items queued up for playback.");
					}
				}
				{
					btnAdd = new JButton();
					panelPlaylist.add(btnAdd);
					btnAdd.setText("Add To Playlist");
					btnAdd.setBounds(0, 315, 247, 28);
					btnAdd.setToolTipText("Adds things to the playback queue.");
					
					btnAdd.addActionListener(new ActionListener() {
						@Override
						public void actionPerformed(ActionEvent e)
						{
							JFileChooser fc = new JFileChooser();
							fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
							fc.setMultiSelectionEnabled(true);
							
							// Check if the user hit cancel.
							if(fc.showOpenDialog(YStreamFrame.this) == JFileChooser.CANCEL_OPTION)
								return;
							
							// TODO: Do something to make sure it is playable?
							
							for(File f : fc.getSelectedFiles())
								addToPlaylist(f);
						}
					});
				}
				{
					btnRemove = new JButton();
					panelPlaylist.add(btnRemove);
					btnRemove.setText("Remove From Playlist");
					btnRemove.setBounds(0, 346, 247, 28);
					btnRemove.setToolTipText("Removes things from the playback queue.");
					
					btnRemove.addActionListener(new ActionListener() {
						@Override
						public void actionPerformed(ActionEvent e)
						{
							// Check if the user selected anything.
							if(playlist.getSelectedValues().length == 0)
								JOptionPane.showMessageDialog(YStreamFrame.this,
								                              "You must highlight something from the playlist to perform this action.",
								                              "Nothing selected",
								                              JOptionPane.ERROR_MESSAGE);
							
							for(Integer i : playlist.getSelectedIndices())
							{
								// Check if the player is playing something and if it's something the user is removing from the playlist.
								// If it is one of the thing that is being removed lets stop the player.
								if(player.isPlaying() && player.getCurrentlyPlaying() == i)
									player.queueMessage(new Message(Priority.LOW, MessageID.PLAYER_STOP, this));
								
								removeFromPlaylist(i);
							}
						}
					});
				}
				{
					btnUp = new JButton();
					panelPlaylist.add(btnUp);
					btnUp.setText("Move Up");
					btnUp.setBounds(0, 377, 247, 28);
					btnUp.setToolTipText("Moves things forward in the playback queue.");
					
					btnUp.addActionListener(new ActionListener() {
						@Override
						public void actionPerformed(ActionEvent e)
						{
							// Check if the user selected anything.
							if(playlist.getSelectedIndices().length == 0)
							{
								JOptionPane.showMessageDialog(YStreamFrame.this,
								                              "You must highlight something from the playlist to perform this action.",
								                              "Nothing selected",
								                              JOptionPane.ERROR_MESSAGE);
								return;
							}
							
							int indices[] = playlist.getSelectedIndices();
							
							moveUpInPlaylist(indices);
							
							// Move to the originally selected index or indices.
							int selected[] = new int[indices.length];
							for(int i = 0; i != indices.length; i++)
								selected[i] = playlist.getMinSelectionIndex() + i - 1;
							
							playlist.setSelectedIndices(selected);
						}
					});
				}
				{
					btnDown = new JButton();
					panelPlaylist.add(btnDown);
					btnDown.setText("Move Down");
					btnDown.setBounds(0, 408, 247, 28);
					btnDown.setToolTipText("Moves things back in the playback queue.");
					
					btnDown.addActionListener(new ActionListener() {
						@Override
						public void actionPerformed(ActionEvent e)
						{
							// Check if the user selected anything.
							if(playlist.getSelectedValues().length == 0)
							{
								JOptionPane.showMessageDialog(YStreamFrame.this,
								                              "You must highlight something from the playlist to perform this action.",
								                              "Nothing selected",
								                              JOptionPane.ERROR_MESSAGE);
								return;
							}
							
							int indices[] = playlist.getSelectedIndices();
							
							moveDownInPlaylist(indices);
							
							// Move to the originally selected index or indices.
							int selected[] = new int[indices.length];
							for(int i = 0; i != indices.length; i++)
								selected[i] = playlist.getMaxSelectionIndex() - i + 1;
							
							playlist.setSelectedIndices(selected);
						}
					});
				}
			}
			{
				panelPlayer = new JPanel();
				getContentPane().add(panelPlayer, BorderLayout.CENTER);
				panelPlayer.setLayout(null);
				{
					textTitle = new JTextField();
					panelPlayer.add(textTitle);
					textTitle.setBounds(6, 6, 472, 28);
					textTitle.setEditable(false);
				}
				{
					canvasScreen = new Canvas();
					panelPlayer.add(canvasScreen);
					canvasScreen.setBounds(6, 37, 472, 304);
					canvasScreen.setBackground(Color.black);
				}
				{
					panelControls = new JPanel();
					panelControls.setLayout(null);
					panelPlayer.add(panelControls);
					panelControls.setBounds(6, 347, 472, 86);
					{
						btnPlay = new JButton();
						btnPlay.setHorizontalAlignment(AbstractButton.CENTER);
						btnPlay.setVerticalAlignment(AbstractButton.CENTER);
						btnPlay.setIcon(new ImageIcon("./lib/Button Icons/Play.png"));
						panelControls.add(btnPlay);
						btnPlay.setBounds(6, 25, 55, 55);
						btnPlay.setToolTipText("Play/Pause current selection.");
						
						btnPlay.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent e)
							{
								// We need to know if the player is not currently playing something.
								// If it's not currently playing something then lets get started.
								// If it is then lets pause it.
								if(player.isPaused() || !player.isPlaying())
									player.queueMessage(new Message(Priority.LOW, MessageID.PLAYER_PLAY, YStreamFrame.this));
								else
									player.queueMessage(new Message(Priority.LOW, MessageID.PLAYER_PAUSE, YStreamFrame.this));
							}
						});
					}
					{
						btnBack = new JButton();
						btnBack.setHorizontalAlignment(AbstractButton.CENTER);
						btnBack.setVerticalAlignment(AbstractButton.CENTER);
						btnBack.setIcon(new ImageIcon("./lib/Button Icons/Back.png"));
						panelControls.add(btnBack);
						btnBack.setBounds(67, 25, 55, 55);
						btnBack.setToolTipText("Goes back to the previous item in the playback queue.");
						
						btnBack.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent e)
							{
								// Check if the player is even playing.
								if(!player.isPlaying() && !player.isPaused())
									return;
								
								player.queueMessage(new Message(Priority.LOW, MessageID.PLAYLIST_BACK, YStreamFrame.this));
							}
						});
					}
					{
						btnStop = new JButton();
						panelControls.add(btnStop);
						btnStop.setHorizontalAlignment(AbstractButton.CENTER);
						btnStop.setVerticalAlignment(AbstractButton.CENTER);
						btnStop.setIcon(new ImageIcon("./lib/Button Icons/Stop.png"));
						btnStop.setBounds(128, 25, 55, 55);
						btnStop.setToolTipText("Stops the current selection.");
						
						btnStop.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent e)
							{
								// We need to know if the player is playing something or not.
								if(!player.isPlaying() && !player.isPaused())
									return;
								
								// Stop whatever is playing.
								player.queueMessage(new Message(Priority.LOW, MessageID.PLAYER_STOP, YStreamFrame.this));
							}
						});
					}
					{
						btnNext = new JButton();
						btnNext.setHorizontalAlignment(AbstractButton.CENTER);
						btnNext.setVerticalAlignment(AbstractButton.CENTER);
						btnNext.setIcon(new ImageIcon("./lib/Button Icons/Next.png"));
						panelControls.add(btnNext);
						btnNext.setBounds(189, 25, 55, 55);
						btnNext.setToolTipText("Jumps to the next item in the playback queue.");
						
						btnNext.addActionListener(new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent e)
							{
								// Check if the player is even playing.
								if(!player.isPlaying() && !player.isPaused())
									return;
								
								player.queueMessage(new Message(Priority.LOW, MessageID.PLAYLIST_NEXT, YStreamFrame.this));
							}
						});
					}
					{
						trackbar = new JSlider();
						panelControls.add(trackbar);
						trackbar.setBounds(6, 6, 460, 19);
						trackbar.setMaximum(1000);
						trackbar.setEnabled(false);
					}
				}
			}
			pack();
			this.setSize(750, 500);
			this.setIconImage(new ImageIcon("./lib/YStream.png").getImage());
			
			// Capture window events for the message handlers.
			addWindowListener(new WindowAdapter() {
				@Override
				public void windowClosing(WindowEvent e)
				{
					// OK now we can notify all the observers.
					notifyObs(new Message(Priority.HIGH, MessageID.KILL, this));
				}
			});
		}
		catch (Exception e)
		{
			JOptionPane.showMessageDialog(null,
			                              "YStream client was unable to open the window for some reason!",
			                              "Error!",
			                              JOptionPane.ERROR_MESSAGE);
			
			// OK we've notified the user so lets close down.
			YStreamClient.getClient().queueMessage(new Message(Priority.HIGH, MessageID.KILL, this));
		}
	}
	
	@Override
	public void paint(Graphics g)
	{
		int width = getWidth();
		int height = getHeight();
		
		// Resize and move the components.
		scrollPlaylist.setBounds(scrollPlaylist.getX(),
		                         scrollPlaylist.getY(),
		                         scrollPlaylist.getWidth(),
		                         height - PLAYLIST_HEIGHT_GROWTH_FACTOR);
		
		btnAdd.setBounds(btnAdd.getX(),
		                 height - ADD_BUTTON_PLACEMENT_FACTOR,
		                 btnAdd.getWidth(),
		                 btnAdd.getHeight());
		
		btnRemove.setBounds(btnRemove.getX(),
		                    height - REMOVE_BUTTON_PLACEMENT_FACTOR,
		                    btnRemove.getWidth(),
		                    btnRemove.getHeight());
		
		btnUp.setBounds(btnUp.getX(),
		                height - UP_BUTTON_PLACEMENT_FACTOR,
		                btnUp.getWidth(),
		                btnUp.getHeight());
		
		btnDown.setBounds(btnDown.getX(),
		                  height - DOWN_BUTTON_PLACEMENT_FACTOR,
		                  btnDown.getWidth(),
		                  btnDown.getHeight());
		
		textTitle.setBounds(textTitle.getX(),
		                    textTitle.getY(),
		                    width - FILE_TITLE_GROWTH_FACTOR,
		                    textTitle.getHeight());
		
		canvasScreen.setBounds(canvasScreen.getX(),
		                       canvasScreen.getY(),
		                       width - SCREEN_GROWTH_X_FACTOR,
		                       height - SCREEN_GROWTH_Y_FACTOR);
		
		panelControls.setBounds(panelControls.getX(),
		                        height - CONTROLS_PLACEMENT_FACTOR,
		                        width - CONTROLS_GROWTH_FACTOR,
		                        panelControls.getHeight());
		
		trackbar.setBounds(trackbar.getX(),
		                   trackbar.getY(),
		                   panelControls.getWidth() - 12,
		                   trackbar.getHeight());
		
		// Finish doing the painting of the screen.
		super.paint(g);
	}
	
	@Override
	public void dispose()
	{
		// We need to stop the position monitor since we're going close down.
		synchronized(positionMonitor) {
			positionMonitor.interrupt();
			try
			{
				positionMonitor.join();
			}
			catch(InterruptedException e)
			{}
		}
		
		// OK now lets get rid of our reference to the player.
		player.unsubscribe(playerListener);
		player = null;
		
		// Now let the window clean itself up.
		super.dispose();
	}
	
	/**
	 * Subscribes a message handler to GUI messages.
	 * 
	 * @param handler - a message handler that will handle GUI messages
	 */
	public void subscribe(MessageHandler handler)
	{
		if(handler == null)
			throw new NullPointerException("Must give valid message handler.");
		
		observers.add(handler);
	}
	
	/**
	 * Unsubscribes a message handler from GUI messages.
	 * 
	 * @param handler - a message handler that is receiving GUI messages
	 */
	public void unsubscribe(MessageHandler handler)
	{
		if(handler == null)
			throw new NullPointerException("Must give valid message handler.");
		
		observers.remove(handler);
	}
	
	/**
	 * Notifies observers of a message.
	 * 
	 * @param msg - the message
	 */
	private void notifyObs(Message msg)
	{
		if(msg == null)
			throw new NullPointerException("Must give a message.");
		
		for(MessageHandler h : observers)
			h.queueMessage(msg);
	}
	
	/**
	 * Adds a file to the playlist.
	 * 
	 * @param file - a file to add to the playlist
	 */
	public void addToPlaylist(File file)
	{
		// TODO: This should only add the file name not the entire path for now.
		// TODO: Eventually it should show a formatted string containing the song name, ablum name, and artist.
		playlistModel.addElement(file.getName());
		
		player.queueMessage(new Message(Priority.LOW,
		                                MessageID.PLAYLIST_ADD,
		                                this,
		                                ByteBufferUtils.stringToByteBuffer(file.getAbsolutePath())));
	}
	
	/**
	 * Removes a file from the playlist.
	 * 
	 * @param index - the index of the item to remove
	 */
	public void removeFromPlaylist(int index)
	{
		playlistModel.remove(index);
		
		player.queueMessage(new Message(Priority.LOW,
		                                MessageID.PLAYLIST_REMOVE,
		                                this,
		                                ByteBuffer.allocate(4).putInt(index)));
	}
	
	/**
	 * Moves a selection of items up in the playlist.
	 * 
	 * @param indices - a selection of items to move
	 */
	public void moveUpInPlaylist(int indices[])
	{
		if(indices == null)
			throw new NullPointerException("Must specify some indices.");
		
		int start = indices[0];
		int end = indices[indices.length - 1];
		int range = end - start + 1;
		
		// Check if the item(s) can be moved up at all.
		if(start == 0)
			return;
		
		for(int i = 0; i != range; i++)
		{
			int i0 = start + i; // The index of the item to move.
			int i1 = start + i - 1; // The new index of the item.
			
			Object t = playlistModel.get(i0);
			playlistModel.set(i0, playlistModel.get(i1));
			playlistModel.set(i1, t);
		}
		
		for(int i : indices)
			player.queueMessage(new Message(Priority.LOW, MessageID.PLAYLIST_UP, this, ByteBuffer.allocate(4).putInt(i)));
	}
	
	/**
	 * Moves a selection of items down in the playlist.
	 * 
	 * @param indices - a selection of items to move
	 */
	public void moveDownInPlaylist(int indices[])
	{
		if(indices == null)
			throw new NullPointerException("Must specify some indices.");
		
		int start = indices[0];
		int end = indices[indices.length - 1];
		int range = end - start + 1;
		
		// Check if the item(s) can be moved down at all.
		if(end == playlistModel.getSize() - 1)
			return;
		
		for(int i = 0; i != range; i++)
		{
			int i0 = end - i; // The index of the item to move.
			int i1 = end - i + 1; // The new index of the item.
			
			Object t = playlistModel.get(i0);
			playlistModel.set(i0, playlistModel.get(i1));
			playlistModel.set(i1, t);
		}
		
		for(int i : indices)
			player.queueMessage(new Message(Priority.LOW, MessageID.PLAYLIST_UP, this, ByteBuffer.allocate(4).putInt(i)));
	}
	
	/**
	 * Clears the playlist.
	 */
	public void clearPlaylist()
	{
		// If the player is playing stop it.
		if(player.isPlaying())
			player.queueMessage(new Message(Priority.MEDIUM, MessageID.PLAYER_STOP, this));
		
		playlistModel.clear();
		player.queueMessage(new Message(Priority.MEDIUM, MessageID.PLAYLIST_CLEAR, this));
	}
	
	/**
	 * Sets the music player reference and then the frame sets the player's video surface in return;
	 * 
	 * @param player - the music player
	 */
	public void setPlayer(YStreamPlayer player)
	{
		if(player == null)
			throw new NullPointerException("Must give a valid player.");
		
		if(this.player != null)
			player.unsubscribe(playerListener);
		
		this.player = player;
		this.player.setVideoSurface(canvasScreen);
		
		// Listen for events!
		playerListener = new MediaPlayerEventAdapter() {
			@Override
			public void stopped(MediaPlayer mediaPlayer)
			{
				btnPlay.setIcon(new ImageIcon("./lib/Button Icons/Play.png"));
				trackbar.setEnabled(false);
			}
			
			@Override
			public void playing(MediaPlayer mediaPlayer)
			{
				btnPlay.setIcon(new ImageIcon("./lib/Button Icons/Pause.png"));
				trackbar.setEnabled(true);
			}
			
			@Override
			public void paused(MediaPlayer mediaPlayer)
			{
				btnPlay.setIcon(new ImageIcon("./lib/Button Icons/Play.png"));
				trackbar.setEnabled(true);
			}
			
			@Override
			public void finished(MediaPlayer mediaPlayer)
			{
				trackbar.setValue(trackbar.getMaximum());
				trackbar.setEnabled(false);
			}
		};
		this.player.subscribe(playerListener);
	}

	/* (non-Javadoc)
	 * @see org.durandj.ystream.client.YStreamClientListener#streamChange(boolean)
	 */
	@Override
	public void streamStateChange(boolean isStreaming)
	{
		if(isStreaming)
		{
			menuConnect.setText("Disconnect from server");
			menuConnect.setToolTipText("Disconnect from the current server.");
		}
		else
		{
			menuConnect.setText("Connect to server");
			menuConnect.setToolTipText("Connect to a YStream server to get a playlist.");
		}
	}
}
